package com.inibb.app.dao;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.inibb.app.dao.api.IActivable;
import com.inibb.app.dao.api.IDao;
import com.inibb.app.dao.api.IEntity;
import com.inibb.app.dao.api.IHiddenable;
import com.inibb.app.dao.api.IInheritable;
import com.inibb.app.dao.criteria.Criteria;
import com.inibb.app.dao.criteria.Order;
import com.inibb.app.dao.criteria.SortOrder;
import com.inibb.app.dao.criteria.projection.Projections;
import com.inibb.app.dao.criteria.restriction.Restrictions;
import com.inibb.app.dao.sqlt.SqlTemplateEngine;
import com.inibb.app.exception.SqlTemplateInitException;
import com.inibb.app.exception.SqlTemplateParseException;


/**
 * Abstract implementation of generic DAO.
 * 
 * @param <E> entity type, it must implements at least <code>IEntity</code>
 * @param <PK> entity's primary key, it must be serializable
 * @see IEntity
 * @since 1.0
 */
public class GenericDao<E, PK extends Serializable> implements IDao<E, PK> {

	private static final String PARAM_SEPARATOR = "_";
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());
	
	private EntityManager entityManager;

	private Class<?> clazz;

	private boolean isDefaultable;

	private boolean isActivable;

	private boolean isHiddenable;

	private boolean isInheritable;

	/**
	 * Default constructor. Use for extend this class.
	 */
	@SuppressWarnings(value = "unchecked")
	public GenericDao() {

		ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
		Type[] types = genericSuperclass.getActualTypeArguments();

		if (types[0] instanceof ParameterizedType) {
			// If the class has parameterized types, it takes the raw type.
			ParameterizedType type = (ParameterizedType) types[0];
			clazz = (Class<?>) type.getRawType();
		} else {
			clazz = (Class<?>) types[0];
		}
		
		checkGenericClass();
	}
	/**
	 * Constructor with given {@link IEntity} implementation. Use for creting DAO without extending
	 * this class.
	 * 
	 * @param clazz class with will be accessed by DAO methods
	 */
	@SuppressWarnings(value = "unchecked")
	public GenericDao(Class<?> clazz) {
		this.clazz = clazz;
		checkGenericClass();
	}

	/**
	 * Set entity manager.
	 * 
	 * @param entityManager entity manager
	 */
	public void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}

	@SuppressWarnings(value = "unchecked")
	@Override
	public E load(PK id)  {
		E entity = get(id);
		return entity;
	}

	@SuppressWarnings(value = "unchecked")
	@Override
	public E get(PK id) {
		return (E) entityManager.find(clazz, id);
	}



	@SuppressWarnings(value = "unchecked")
	@Override
	public List<E> get(IInheritable<E> parent) {
		if (parent == null) {
			return findByCriteria(Criteria.forClass(clazz)
					.add(Restrictions.isNull(IInheritable.P_PARENT)));
		} else {
			return findByCriteria(Criteria.forClass(clazz).add(Restrictions.eq(IInheritable.P_PARENT,
					parent)));
		}
	}

	@SuppressWarnings(value = "unchecked")
	@Override
	public List<E> getAll() {
		return findByCriteria(Criteria.forClass(clazz));
	}

	public List<E> findByExample(E example, String orderProperty, SortOrder sortOrder) {
		Criteria criteria = buildCriteria(example);
		if(!StringUtils.isBlank(orderProperty)){
			if(SortOrder.ascending == sortOrder){
				criteria.addOrder(Order.asc(orderProperty));
			}
			if(SortOrder.descending == sortOrder){
				criteria.addOrder(Order.desc(orderProperty));
			}
		}
		
		return findByCriteria(criteria);
	}
	
	public List<E> findByExample(E example, String orderProperty, SortOrder sortOrder, int startRow, int pageSize) {
		Criteria criteria = buildCriteria(example);
		if(!StringUtils.isBlank(orderProperty)){
			if(SortOrder.ascending == sortOrder){
				criteria.addOrder(Order.asc(orderProperty));
			}
			if(SortOrder.descending == sortOrder){
				criteria.addOrder(Order.desc(orderProperty));
			}
		}
		criteria.setFirstResult(startRow);
		criteria.setMaxResults(pageSize);
		
		return findByCriteria(criteria);
	}
	
	@SuppressWarnings(value = "unchecked")
	@Override
	public List<E> findByExample(E example) {
		Criteria criteria = buildCriteria(example);
		return findByCriteria(criteria);
	}

	

	@Override
	public E merge(final E object) {
		return entityManager.merge(object);
	}

	public E persist(final E object) {
		entityManager.persist(object);
		return object;
}

	@Override
	public void delete(final PK id) throws UnsupportedOperationException {
		delete(load(id));
	}


	@Override
	public void delete(final E object) throws UnsupportedOperationException {
		if (isHiddenable) {
			((IHiddenable) object).setHidden(true);
			entityManager.merge(object);
		} else {
			entityManager.remove(object);
		}
	}

	@Override
	public void delete(final E... objects) throws UnsupportedOperationException {
		deleteAll(Arrays.asList(objects), true);
	}

	@Override
	public void deleteAll() throws UnsupportedOperationException {
		deleteAll(getAll(), false);
	}

	private void deleteAll(final Collection<E> objects, boolean checkIdDefault)
	throws UnsupportedOperationException
	{
		if (isHiddenable) {
			for (E object : objects) {
				((IHiddenable) object).setHidden(true);
				entityManager.merge(object);
			}
		} else {
			for (E object : objects) {
				entityManager.remove(object);
			}
		}
	}


	private void checkGenericClass() {
		for (Class i : clazz.getInterfaces()) {
			if (i == IActivable.class) {
				isActivable = true;
			} else if (i == IHiddenable.class) {
				isHiddenable = true;
			} else if (i == IInheritable.class) {
				isInheritable = true;
			}
		}
	}

	@Override
	public void refresh(final E entity) {
		entityManager.refresh(entity);
	}

	@Override
	public void flushAndClear() {
		entityManager.flush();
		entityManager.clear();
	}

	/**
	 * Retrieve objects using criteria. It is equivalent to <code>criteria.list(entityManager)</code>.
	 * 
	 * @param criteria criteria which will be executed
	 * @return list of founded objects
	 * @see javax.persistence.Query#getResultList()
	 */
	@SuppressWarnings(value = "unchecked")
	public List findByCriteria(Criteria criteria) {
		return criteria.list(entityManager);
	}

	
	protected int countByCriteria(Criteria criteria) {
		return ((Long)criteria.uniqueResult(entityManager)).intValue();
	}

	/**
	 * Retrieve an unique object using criteria. It is equivalent to
	 * <code>criteria.uniqueResult(entityManager)</code>.
	 * 
	 * @param criteria criteria which will be executed
	 * @return retrieved object
	 * @throws NoResultException - if there is no result
	 * @throws NonUniqueResultException - if more than one result
	 * @see javax.persistence.Query#getSingleResult()
	 */
	public Object findUniqueByCriteria(Criteria criteria)
	throws NonUniqueResultException,
	NoResultException
	{
		return criteria.uniqueResult(entityManager);
	}

	@Override
	public boolean isActivable() {
		return isActivable;
	}



	@Override
	public boolean isHiddenable() {
		return isHiddenable;
	}

	@Override
	public boolean isInheritable() {
		return isInheritable;
	}

	/**
	 * Get entity manager.
	 * 
	 * @return entity manager
	 */
	protected EntityManager getEntityManager() {
		return entityManager;
	}

	@Override
	public List<E> findByNamedQuery(String namedQuery) {
		return findByNamedQuery(namedQuery, true);
	}

	public List<E> findByNamedQuery(String namedQuery, boolean withUR) {
		Query query = entityManager.createNamedQuery(namedQuery);
		return query.getResultList();
	}
	
	@Override
	public int countByExample(E example) {
		return countByExample(example, true);
	}
	
	public int countByExample(E example, boolean withUR) {
		Criteria criteria = buildCriteria(example);
		criteria.setProjection(Projections.rowCount());
		return countByCriteria(criteria);
		
	}
	
	private Criteria buildCriteria(E example){
		example = (E)handleFuzzyQueryCriteria(example);

		Criteria criteria = Criteria.forClass(clazz);

		Field[] fields = example.getClass().getDeclaredFields();
		Method[] methods = example.getClass().getDeclaredMethods();

		for (Field field : fields) {
			if (field.getName().equals(IActivable.P_IS_ACTIVE)) {
				continue;
			}
			if (field.getName().equals(IHiddenable.P_IS_HIDDEN)) {
				continue;
			}
			if (!field.isAnnotationPresent(Column.class) && !field.isAnnotationPresent(Basic.class)) {
				continue;
			}

			Object value = null;

			try {
				field.setAccessible(true);
				value = field.get(example);
			} catch (IllegalArgumentException e) {
				continue;
			} catch (IllegalAccessException e) {
				continue;
			}

			if (value == null) {
				continue;
			}
			if(value instanceof String){
				if(StringUtils.isBlank((String)value)){
					continue;
				}
				
				if(isFuzzyQuery((String)value)){
					criteria.add(Restrictions.like(field.getName(), value));		
				}else{
					criteria.add(Restrictions.eq(field.getName(), value));	
				}
				
			}else{
				criteria.add(Restrictions.eq(field.getName(), value));		
			}
			
			
			
			
		}
		
		//handle annotation in getter methods...
		for (Method method : methods) {
			if (method.getName().equalsIgnoreCase(IActivable.P_IS_ACTIVE)) {
				continue;
			}
			if (method.getName().equalsIgnoreCase(IHiddenable.P_IS_HIDDEN)) {
				continue;
			}
			if (!method.isAnnotationPresent(Column.class) && !method.isAnnotationPresent(Basic.class)) {
				continue;
			}

			Object value = null;

			try {
				method.setAccessible(true);
				value = method.invoke(example);
			} catch (IllegalArgumentException e) {
				continue;
			} catch (IllegalAccessException e) {
				continue;
			} catch (InvocationTargetException e) {
				continue;
			}

			if (value == null) {
				continue;
			}

			String property = method.getName().replaceFirst("get", StringUtils.EMPTY);
//			System.out.println("resolved property = "+ property+"-"+StringUtils.uncapitalize(property));
			
			if(value instanceof String){
				if(StringUtils.isBlank((String)value)){
					continue;
				}
				
				if(isFuzzyQuery((String)value)){
					criteria.add(Restrictions.like(StringUtils.uncapitalize(property), value));
				}else{
					criteria.add(Restrictions.eq(StringUtils.uncapitalize(property), value));
				}
				
			}else{
				criteria.add(Restrictions.eq(StringUtils.uncapitalize(property), value));
			}

			
			
		}
		if (example instanceof IInheritable) {
			if (((IInheritable) example).getParent() == null) {
				criteria.add(Restrictions.isNull(IInheritable.P_PARENT));
			} else {
				criteria.add(Restrictions.eq(IInheritable.P_PARENT, ((IInheritable) example).getParent()));
			}
		}
		
		return criteria;
	}
	
	  /**
     * check whether the search criteria indicates to perform a fuzzy query. 
     * @param searchCriteria
     * @return
     */
    public boolean isFuzzyQuery(String searchCriteria){
    	
    	if(StringUtils.isEmpty(searchCriteria)) return false;
    	if(searchCriteria.indexOf("%")!=-1) return true;
    	   	    	
    	return false;
    }

	@Override
	public String getJpqlTemplate(String namespace, String sqltId, Object criteria) {
		try {
			return SqlTemplateEngine.getInstance().getJpql(namespace, sqltId, criteria);
		} catch (SqlTemplateParseException e) {
			throw new RuntimeException(e);
		} catch (SqlTemplateInitException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public String getNativeSqlTemplate(String namespace, String sqltId, Object criteria) {
		try {
			return SqlTemplateEngine.getInstance().getNativeSql(namespace, sqltId, criteria);
		} catch (SqlTemplateParseException e) {
			throw new RuntimeException(e);
		} catch (SqlTemplateInitException e) {
			throw new RuntimeException(e);
		}
	}
	
	@Override
	public Object queryNativeSqlForObject(String namespace, String sqltId, Object criteria){
		return queryNativeSqlForObject(namespace, sqltId, criteria, true);
	}
	
	public Object queryNativeSqlForObject(String namespace, String sqltId, Object criteria, boolean withUR){
		
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createNativeQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.getSingleResult();
	}
	
	
	@Override
	public List queryNativeSqlForList(String namespace, String sqltId, Object criteria){
		return queryNativeSqlForList(namespace, sqltId, criteria, true);
	}
	
	public List queryNativeSqlForList(String namespace, String sqltId, Object criteria, boolean withUR){
		
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createNativeQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.getResultList();
	}
	
	public List queryNativeSqlForList(String namespace, String sqltId, Object[] criteria){
		return queryNativeSqlForList(namespace, sqltId, criteria, true);
	}
	
	public List queryNativeSqlForList(String namespace, String sqltId, Object[] criteria, boolean withUR){
		
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createNativeQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.getResultList();
	}
	

	
	public List queryNativeSqlForList(String namespace, String sqltId,
			Object criteria, int startRow, int pageSize, boolean withUR){
		
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createNativeQuery(qlString);
		setParameters(query, qlString, criteria);
		query.setFirstResult(startRow);
		query.setMaxResults(pageSize);
		return query.getResultList();
		
	}
	@Override
	public List queryNativeSqlForList(String namespace, String sqltId,
			Object criteria, int startRow, int pageSize) {
		return queryNativeSqlForList(namespace, sqltId, criteria, startRow, pageSize, true);
	}
	
	@Override
	public int executeNativeSqlUpdate(String namespace, String sqltId,
			Object criteria) {
		String qlString = this.getNativeSqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createNativeQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.executeUpdate();
	}
	
	@Override
	public int executeJpqlUpdate(String namespace, String sqltId,
			Object criteria) {
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.executeUpdate();
	}
	
	
	@Override
	public Object queryJpqlForObject(String namespace, String sqltId, Object criteria){
		return queryJpqlForObject(namespace, sqltId, criteria, true);
	}
	
	public Object queryJpqlForObject(String namespace, String sqltId, Object criteria, boolean withUR){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.getSingleResult();
	}
	
	@Override
	public List queryJpqlForList(String namespace, String sqltId, Object criteria){
		return queryJpqlForList(namespace, sqltId, criteria, true);
	}
    
	
	public List queryJpqlForList(String namespace, String sqltId, Object criteria, boolean withUR){
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createQuery(qlString);
		setParameters(query, qlString, criteria);
		return query.getResultList();
	}
    
	
	@Override
	public List queryJpqlForList(String namespace, String sqltId, Object criteria, int startRow, int pageSize){
		return queryJpqlForList(namespace, sqltId, criteria, startRow, pageSize, true);
	}
	
	public List queryJpqlForList(String namespace, String sqltId, Object criteria, int startRow, int pageSize, boolean withUR){
		
		criteria = handleFuzzyQueryCriteria(criteria);
		String qlString = this.getJpqlTemplate(namespace, sqltId, criteria);
		Query query = this.entityManager.createQuery(qlString);
		setParameters(query, qlString, criteria);
		query.setFirstResult(startRow);
		query.setMaxResults(pageSize);
		return query.getResultList();
	}
	
	private void setParameters(Query query, String qlString, Object criteria){
		List<String> params =  this.resolveNamedParameters(qlString);
		try {
			for(String param: params){
				Object value = resolveParameterValue(param, criteria);
				query.setParameter(param, value);
				logger.trace("setting query parameter: {} = {}", param, value);
			}
			
		} catch (SqlTemplateParseException e) {
			throw new RuntimeException(e);
		}
		
	}
	private void setParameters(Query query, String qlString, Object[] criteria){
		List<String> params =  this.resolveNamedParameters(qlString);		
		int i=0;
		for(String param: params){				
			query.setParameter(param, criteria[i]);
			logger.trace("setting query parameter: {} = {}", param,  criteria[i]);
			i++;
		}	
	}
	
	private List<String> resolveNamedParameters(String qlString){
		List<String> params = new ArrayList<String>(0);
		String[] qlStrs = StringUtils.splitByWholeSeparator(qlString, null);
		for(String ql: qlStrs){
			if(ql!=null && ql.startsWith(":")){
				params.add(ql.replaceFirst(":", ""));
			}
		}
		return params;
	}
	
	private Object resolveParameterValue(String property, Object object) throws SqlTemplateParseException{
		try {
			if(property.indexOf(PARAM_SEPARATOR)!=-1){
				String resolvedProp = property.replaceAll(PARAM_SEPARATOR, "."); 
				return PropertyUtils.getNestedProperty(object, resolvedProp);
			}else{
				return PropertyUtils.getProperty(object, property);
			}
		} catch (IllegalAccessException e) {
			throw new SqlTemplateParseException(e);
		} catch (InvocationTargetException e) {
			throw new SqlTemplateParseException(e);
		} catch (NoSuchMethodException e) {
			throw new SqlTemplateParseException(e);
		}
	}
	
	

	
	public Object handleFuzzyQueryCriteria(Object example) {
		List<Integer> checkList = new ArrayList<Integer>();
		return traverseCriteriaObject(example, checkList);
	}
	
	private Object traverseCriteriaObject(Object example, List<Integer> checkList){

		
		if(example==null){
			return null;
		}
		
		//to avoid duplicated object traveling.
		if(checkList.contains(example.hashCode())){
			logger.trace("Object {} has been handled.", example);
			return example;
		}else{
			checkList.add(example.hashCode());
		}
		
		
		if(example instanceof Map){
			logger.trace("BeanUtils can not clone Map object.");
			return example;
		}
		
		
		if(example instanceof Collection){
			logger.trace("BeanUtils can not clone Collection object.");
			return example;
		}
		
		logger.trace("start to handle fuzzy query field....");
		Object cle = example;

		try {
			
			
			logger.trace("Object before handling: {}", BeanUtils.describe(example).toString());
			cle = BeanUtils.cloneBean(example);
			Field[] fields = cle.getClass().getDeclaredFields();
			Method[] methods = cle.getClass().getDeclaredMethods();
			for (Field field : fields) {
				boolean isFuzzyField = false;//field.isAnnotationPresent(FuzzyQuery.class);
				if (isFuzzyField) {
					trimFieldValue(example, field);
					trimAndFuzzyFieldValue(cle, field);
				}else{
					trimFieldValue(example, field);
					trimFieldValue(cle, field);
				}
				
				if ((IEntity.class).isAssignableFrom(field.getType())){
					logger.trace("Found PO attribute {}, start traversing PO field.", field.getName());
					Object value = null;
					field.setAccessible(true);
					value = field.get(example);
					if(value!=null){
						Object newVal = traverseCriteriaObject(value, checkList);
						field.set(cle, newVal);
					}
				} 
			}
			//handle annotation in getter methods...
			for (Method method : methods) {
				method.setAccessible(true);
				boolean isFuzzyField = false;//method.isAnnotationPresent(FuzzyQuery.class);
				if (isFuzzyField) {
					if (method.getReturnType().isAssignableFrom(String.class)) {
						Object value = null;
						method.setAccessible(true);
						value = method.invoke(example);

						String property = StringUtils.uncapitalize(method.getName().replaceFirst("get", StringUtils.EMPTY));
						if(!StringUtils.isBlank((String)value)){
							Object newVal = "%" + StringUtils.trimToEmpty((String)value) + "%";
							BeanUtils.setProperty(cle, property, newVal);
						}
					}
				}
				

				
			}
			logger.trace("Object after handling: {}", BeanUtils.describe(cle).toString());
		} catch (Exception e) {
			logger.error("Error handling fuzzy query.", e);
		}
		
		return cle;
		
	}
	
	private void trimFieldValue(Object example, Field field) throws Exception{
		if (field.getType().isAssignableFrom(String.class)) {
			Object value = null;
			field.setAccessible(true);
			value = field.get(example);
			if(!StringUtils.isBlank((String)value)){
				Object newVal = StringUtils.trimToEmpty((String)value);
				field.set(example, newVal);
			}
		}
	}


	private void trimAndFuzzyFieldValue(Object example, Field field) throws Exception{
		if (field.getType().isAssignableFrom(String.class)) {
			Object value = null;
			field.setAccessible(true);
			value = field.get(example);
			if(!StringUtils.isBlank((String)value)){
				Object newVal = "%" + StringUtils.trimToEmpty((String)value)+ "%";
				field.set(example, newVal);
			}
		}
	}	

}
